package com.shisan.tingshu.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.LongTermsAggregate;
import co.elastic.clients.elasticsearch._types.aggregations.LongTermsBucket;
import co.elastic.clients.elasticsearch._types.aggregations.TopHitsAggregate;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.NestedQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.*;
import co.elastic.clients.json.JsonData;
import com.alibaba.fastjson.JSONObject;
import com.shisan.tingshu.album.client.AlbumInfoFeignClient;
import com.shisan.tingshu.common.execption.ShisanException;
import com.shisan.tingshu.common.result.Result;
import com.shisan.tingshu.model.album.BaseCategory3;
import com.shisan.tingshu.model.search.AlbumInfoIndex;
import com.shisan.tingshu.model.search.SuggestIndex;
import com.shisan.tingshu.query.search.AlbumIndexQuery;
import com.shisan.tingshu.search.service.SearchService;
import com.shisan.tingshu.vo.search.AlbumInfoIndexVo;
import com.shisan.tingshu.vo.search.AlbumSearchResponseVo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;


@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class SearchServiceImpl implements SearchService {

    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    /**
     * 频道页数据展示
     * @param c1Id
     * @return
     */
    @SneakyThrows
    @Override
    public List<Map<String, Object>> channel(Long c1Id) {

        // 根据一级分类id查询该一级分类下的置顶的三级分类集合
        Result<List<BaseCategory3>> category3ListByC1Id = albumInfoFeignClient.getBaseCategory3ListByC1Id(c1Id);
        List<BaseCategory3> baseCategory3ListData = category3ListByC1Id.getData();
        if (CollectionUtils.isEmpty(baseCategory3ListData)) {
            throw new ShisanException(201, "远程调用分类信息失败");
        }

        // 过滤得到三级分类的id集合，并转成FieldValue类型（ES 在查询的时候要用到这个类型）
        List<FieldValue> c3IdsFieldValue = baseCategory3ListData.stream().map(baseCategory3 -> {
            return FieldValue.of(baseCategory3.getId());
        }).collect(Collectors.toList());

        // 将三级分类集合转成map
        Map<Long, BaseCategory3> c3IdAndBaseCategory3 = baseCategory3ListData.stream().collect(Collectors.toMap(BaseCategory3::getId, v -> v));

        // 构建 DSL 语句
        SearchRequest searchRequest = buildChannelDsl(c3IdsFieldValue);

        // 查询 ES
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(searchRequest, AlbumInfoIndex.class);

        // 解析 ES 返回的数据
        List<Map<String, Object>> result = parseChannelData(response, c3IdAndBaseCategory3);

        return result;
    }

    /**
     * 带条件的搜索
     * @param albumIndexQuery
     * @return
     */
    @SneakyThrows
    @Override
    public AlbumSearchResponseVo search(AlbumIndexQuery albumIndexQuery) {

        // 构建带条件的DSL语句
        SearchRequest searchRequest = buildSearchDsl(albumIndexQuery);

        // 查询 ES，得到响应对象
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(searchRequest, AlbumInfoIndex.class);

        // 解析 ES 返回的数据，封装成前端需要的数据格式
        AlbumSearchResponseVo albumSearchResponseVo = parseSearchData(response);
        albumSearchResponseVo.setPageSize(albumIndexQuery.getPageSize());
        albumSearchResponseVo.setPageNo(albumIndexQuery.getPageNo());
        Long total = albumSearchResponseVo.getTotal();
        Long totalPages = total % albumIndexQuery.getPageSize() == 0 ? total / albumIndexQuery.getPageSize() : total / albumIndexQuery.getPageSize() + 1;
        albumSearchResponseVo.setTotalPages(totalPages); // 总页数

        // 返回数据给前端
        return albumSearchResponseVo;
    }

    /**
     * 搜索关键字自动补全
     * @param input
     * @return
     */
    @SneakyThrows
    @Override
    public Set<String> completeSuggest(String input) {

        // 构建 DSL 语句
        SearchRequest searchRequest = buildCompletionDsl(input);
        // 开始查询
        SearchResponse<SuggestIndex> response = elasticsearchClient.search(searchRequest, SuggestIndex.class);
        // 解析数据
        Set<String> set = parseCompletionData(response);

        // 上面的查询是根据用户的输入，从索引库中查询开头与用户输入匹配的文档
        // 如果不足10条数据，会从索引库中查询包含用户输入的文档
        if (set.size() < 10) {
            SearchResponse<SuggestIndex> searchResponse = elasticsearchClient
                    .search(sb -> sb
                            .index("suggestinfo")
                            .query(qb -> qb
                                    .match(mqb -> mqb
                                            .field("title")
                                            .query(input))), SuggestIndex.class);
            for (Hit<SuggestIndex> hit : searchResponse.hits().hits()) {
                SuggestIndex suggestIndex = hit.source();
                String title = suggestIndex.getTitle();
                set.add(title);
                if (set.size() >= 10)
                    break;
            }
        }

        return set;
    }

    /**
     * 频道页数据展示：构建 DSL 语句。查询当前一级分类下的三级分类集合中，每个三级分类对象中6个热度值最高的专辑信息
     *
     * @param c3IdsFieldValue
     * @return
     */
    private SearchRequest buildChannelDsl(List<FieldValue> c3IdsFieldValue) {
        // 对应的 DSL 语句如下：
        // POST /albuminfo/_search # 对索引名为"albuminfo"的数据执行搜索操作
        //{
        //  "query": { # 查询部分，用于筛选文档
        //    "terms": { # 使用terms查询，匹配字段值在给定列表中的文档
        //      "category3Id": [ # 针对category3Id字段进行匹配
        //        1001,
        //        1002,
        //        1003,
        //        1004,
        //        1005,
        //        1006,
        //        1007
        //      ] # 只查询category3Id值为这7个值之一的文档
        //    }
        //  },
        //  "aggregations": { # 聚合部分，用于对查询结果进行统计分析。可简写为 aggs
        //    "category3IdAgg": { # 定义一个名为category3IdAgg的聚合
        //      "aggregations": { # 子聚合，在父聚合的基础上进一步聚合
        //        "topHitHotScoreAgg": { # 定义一个名为topHitHotScoreAgg的子聚合
        //          "top_hits": { # 使用top_hits聚合，获取每组中的前几条文档
        //            "size": 6, # 每组返回6条文档
        //            "sort": [ # 排序规则
        //              {
        //                "hotScore": { # 按照hotScore字段排序
        //                  "order": "desc" # 降序排列(从高到低)
        //                }
        //              }
        //            ]
        //          }
        //        }
        //      },
        //      "terms": { # 使用terms聚合，按字段值分组
        //        "field": "category3Id", # 按category3Id字段分组
        //        "size": 7 # 返回7个分组(对应查询中的7个category3Id值)
        //      }
        //    }
        //  }
        //}

        // 构建 SearchRequestBuilder 对象
        SearchRequest.Builder builder = new SearchRequest.Builder();
        // 设置 DSL 语句，构建查询
        builder.index("albuminfo")
                .query(b -> b
                        .terms(tqb -> tqb
                                .field("category3Id")
                                .terms(tqfb -> tqfb.value(c3IdsFieldValue))))
                .aggregations("category3IdAgg", ab -> ab
                        .terms(tab -> tab
                                .field("category3Id")
                                .size(c3IdsFieldValue.size()))
                        .aggregations("topHitHotScoreAgg", sab -> sab
                                .topHits(thab -> thab
                                        .sort(sob -> sob
                                                .field(fsb -> fsb
                                                        .field("hotScore")
                                                        .order(SortOrder.Desc)))
                                        .size(6))));

        SearchRequest searchRequest = builder.build();
        System.out.println("频道页的 DSL 语句：" + searchRequest.toString());

        return searchRequest;
    }

    /**
     * 频道页数据展示：解析 ES 返回的数据
     *
     * @param response
     * @param c3IdAndBaseCategory3
     * @return
     */
    private List<Map<String, Object>> parseChannelData(SearchResponse<AlbumInfoIndex> response, Map<Long, BaseCategory3> c3IdAndBaseCategory3) {
        ArrayList<Map<String, Object>> result = new ArrayList<>();

        // 得到category3IdAgg聚合对象
        Aggregate category3IdAgg = response.aggregations().get("category3IdAgg");
        // 得到Aggregate具体的类型
        LongTermsAggregate longTermsAggregate = category3IdAgg.lterms();
        // 得到父聚合的桶
        List<LongTermsBucket> buckets = longTermsAggregate.buckets().array();
        // 循环遍历父聚合的桶
        for (LongTermsBucket bucket : buckets) {
            // 得到子聚合
            Aggregate topHitHotScoreAgg = bucket.aggregations().get("topHitHotScoreAgg");
            // 把子聚合转为具体的类型
            TopHitsAggregate topHitsAggregate = topHitHotScoreAgg.topHits();
            ArrayList<AlbumInfoIndex> albumInfoIndices = new ArrayList<>();
            for (Hit<JsonData> hit : topHitsAggregate.hits().hits()) {
                JsonData source = hit.source();
                AlbumInfoIndex albumInfoIndex = JSONObject.parseObject(source.toString(), AlbumInfoIndex.class);
                albumInfoIndices.add(albumInfoIndex);
            }

            HashMap<String, Object> map = new HashMap<>();
            Long category3Id = bucket.key();
            map.put("baseCategory3", c3IdAndBaseCategory3.get(category3Id)); // 当前三级分类对象
            map.put("list", albumInfoIndices); // 当前三级分类下热度值最高的6个专辑集合
            result.add(map);
        }

        return result;
    }

    /**
     * 带条件的搜索：构建 DSL 语句。
     * @param albumIndexQuery
     * @return
     */
    private SearchRequest buildSearchDsl(AlbumIndexQuery albumIndexQuery) {
        // 对应的 DSL 语句如下：

        // 关键字搜索、根据一级/二级/三级 id 分类搜索、标签（可能有多个）搜索
        // 第一层：构建 SearchRequest.Builder 对象
        SearchRequest.Builder searchBuilder = new SearchRequest.Builder();
        // 第二层：构建 BoolQuery.Builder 对象
        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
        // 判断 albumIndexQuery 中是否携带了关键字
        String keyword = albumIndexQuery.getKeyword();
        if(!StringUtils.isEmpty(keyword)){
            boolQueryBuilder.should(qb->qb.match(mqb->mqb.field("albumTitle").query(keyword)));
            boolQueryBuilder.should(qb->qb.match(mqb->mqb.field("albumIntro").query(keyword)));
            boolQueryBuilder.should(qb->qb.match(mqb->mqb.field("announcerName").query(keyword)));
        }
        // 判断分类id
        // 判断一级分类id是否携带
        Long category1Id = albumIndexQuery.getCategory1Id();
        if(category1Id != null){
            boolQueryBuilder.filter(qb->qb.term(tqb->tqb.field("category1Id").value(category1Id)));
        }
        // 判断二级分类id是否携带
        Long category2Id = albumIndexQuery.getCategory2Id();
        if(category2Id != null){
            boolQueryBuilder.filter(qb->qb.term(tqb->tqb.field("category2Id").value(category2Id)));
        }
        // 判断三级分类id是否携带
        Long category3Id = albumIndexQuery.getCategory3Id();
        if(category3Id != null){
            boolQueryBuilder.filter(qb->qb.term(tqb->tqb.field("category3Id").value(category3Id)));
        }
        // 判断标签是否携带。格式形如："1:2"，即"属性id:属性值id"
        List<String> attributeList = albumIndexQuery.getAttributeList();
        if(!CollectionUtils.isEmpty(attributeList)){
            for(String attrAndValueId : attributeList){
                String[] split = attrAndValueId.split(":");
                String attrId = split[0]; // 属性id
                String valueId = split[1]; // 属性值id
                NestedQuery.Builder builder = new NestedQuery.Builder();
                builder.path("attributeValueIndexList")
                        .query(qb ->qb
                                .bool(bqb->bqb
                                        .must(mqb->mqb
                                                .term(tqb->tqb
                                                        .field("attributeValueIndexList.attributeId")
                                                        .value(attrId)))
                                        .must(mqb->mqb
                                                .term(tqb->tqb
                                                        .field("attributeValueIndexList.valueId")
                                                        .value(valueId)))));
                NestedQuery nestedQuery = builder.build();
                boolQueryBuilder.must(qb->qb.nested(nestedQuery));
            }
        }
        // 通过 BoolQuery.Builder 对象构建 BoolQuery 对象
        BoolQuery boolQuery = boolQueryBuilder.build();
        // 通过 _toQuery() 方法将 BoolQuery 对象转成 Query 对象
        Query query = boolQuery._toQuery();
        // 将上面得到的 query 对象设置到 SearchRequest 对象中
        searchBuilder.index("albuminfo").query(query);

        // 分页查询
        Integer pageNo = albumIndexQuery.getPageNo(); // 页码，默认1
        Integer pageSize = albumIndexQuery.getPageSize(); // 每页显示的条数，默认10
        Integer from = (pageNo - 1) * pageSize; // 从第几条数据开始查询
        searchBuilder.from(from).size(pageSize);

        // 排序。格式形如："1:desc[asc]"，即"排序维度:排序规则"。1代表热度值（综合排序），2代表播放量，3代表发布时间
        String order = albumIndexQuery.getOrder();
        if(!StringUtils.isEmpty(order)) {
            String[] split = order.split(":");
            String orderDimension = split[0]; // 排序维度
            String orderRule = split[1]; // 排序规则
            SortOrder sortOrder = orderRule.equals("desc") ? SortOrder.Desc : SortOrder.Asc;
            switch (orderDimension) {
                case "1":
                    // 热度值排序
                    searchBuilder.sort(sob -> sob
                            .field(fsb -> fsb
                                    .field("hotScore")
                                    .order(sortOrder)));
                    break;
                case "2":
                    // 播放量排序
                    searchBuilder.sort(sob -> sob
                            .field(fsb -> fsb
                                    .field("playStatNum")
                                    .order(sortOrder)));
                    break;
                case "3":
                    // 发布时间排序
                    searchBuilder.sort(sob -> sob
                            .field(fsb -> fsb
                                    .field("createTime")
                                    .order(sortOrder)));
                    break;
                default:
                    break;
            }
        } else {
            // 默认按热度值降序排序
            searchBuilder.sort(sob -> sob
                    .field(fsb -> fsb
                            .field("hotScore")
                            .order(SortOrder.Desc)));
        }

        // 组合高亮
        searchBuilder.highlight(hb -> hb.fields("albumTitle", hfb -> hfb.preTags("<font style='color:red'>").postTags("</font>")));

        SearchRequest searchRequest = searchBuilder.build();
        System.out.println("搜索的 DSL 语句：" + searchRequest.toString());

        return searchRequest;
    }

    /**
     * 带条件的搜索：解析 ES 返回的数据
     *
     * @param response
     * @return
     */
    private AlbumSearchResponseVo parseSearchData(SearchResponse<AlbumInfoIndex> response) {

        AlbumSearchResponseVo albumSearchResponseVo = new AlbumSearchResponseVo();

        TotalHits totalHits = response.hits().total();
        long value = totalHits.value();
        albumSearchResponseVo.setTotal(value); // 总专辑数

        List<AlbumInfoIndexVo> albumInfoIndexVoList = response.hits().hits().stream().map(albumInfoIndexHit -> {
            AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
            AlbumInfoIndex albumInfoIndex = albumInfoIndexHit.source();
            BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);
            // 处理高亮
            Map<String, List<String>> highlight = albumInfoIndexHit.highlight();
            List<String> albumTitle = highlight.get("albumTitle");
            if(!CollectionUtils.isEmpty(albumTitle)){
                String highlightAlbumTitle = albumTitle.get(0);
                albumInfoIndexVo.setAlbumTitle(highlightAlbumTitle); // 处理高亮
            }
            return albumInfoIndexVo;
        }).collect(Collectors.toList());

        albumSearchResponseVo.setList(albumInfoIndexVoList); // 专辑集合
        return albumSearchResponseVo;
    }

    /**
     * 搜索关键字自动补全：构建 DSL 语句。
     * @return
     */
    private SearchRequest buildCompletionDsl(String input)  {

        // 创建 SearchRequestBuilder 对象
        SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();
        // 创建 SuggesterBuilder 对象
        Suggester.Builder suggesterBuilder = new Suggester.Builder();
        suggesterBuilder
                .suggesters("suggestionKeyword", fsb -> fsb
                        .prefix(input)
                        .completion(csb -> csb.field("keyword")))
                .suggesters("suggestionKeywordPinyin", fsb -> fsb
                        .prefix(input)
                        .completion(csb -> csb.field("keywordPinyin")))
                .suggesters("suggestionKeywordSequence", fsb -> fsb
                        .prefix(input)
                        .completion(csb -> csb.field("keywordSequence")));
        // 将 SuggesterBuilder 对象添加到 SearchRequestBuilder 对象中
        Suggester suggester = suggesterBuilder.build();
        // 将 SearchRequestBuilder 对象添加到 SearchRequest 对象中
        SearchRequest searchRequest = searchRequestBuilder.index("suggestinfo").suggest(suggester).build();
        log.info("搜索关键字自动补全的 DSL 语句：" + searchRequest.toString());

        return searchRequest;
    }

    /**
     * 搜索关键字自动补全：解析 ES 返回的数据
     * @param response
     * @return
     */
    private Set<String> parseCompletionData(SearchResponse<SuggestIndex> response) {

        HashSet<String> result = new HashSet<>();
        // Map中有三对key-value
        // 第一对: key：suggestionKeyword，value：List<满足条件的内容>
        // 第二对: key：suggestionKeywordPinyin，value：List<满足条件的内容>
        // 第三对: key：suggestionKeywordSequence，value：List<满足条件的内容>
        Map<String, List<Suggestion<SuggestIndex>>> map = response.suggest();
        // 遍历 map，三次
        for (Map.Entry<String, List<Suggestion<SuggestIndex>>> stringListEntry : map.entrySet()) {
            List<Suggestion<SuggestIndex>> value = stringListEntry.getValue();
            // 遍历 value，一次
            for (Suggestion<SuggestIndex> suggestIndexSuggestion : value) {
                List<CompletionSuggestOption<SuggestIndex>> options = suggestIndexSuggestion.completion().options();
                for (CompletionSuggestOption<SuggestIndex> option : options) {
                    SuggestIndex suggestIndex = option.source();
                    String title = suggestIndex.getTitle();
                    result.add(title); // 使用 HashSet 集合去重
                }
            }
        }

        return result;
    }
}
